<?php
/**
 * Abstract table for data joining with tree table
 * by Zend_Db_Tree_Nested object
 * 
 * @category   Zend
 * @package    Zend_Db
 * @author     Steshenko Alexander (http://lcf.name)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Db_Tree_Nested_Table extends Zend_Db_Table_Abstract
{
    /**
     * Configuration array key for nested sets custom object
     *
     */
    const NESTED = 'nested';
    
    /*
     * TODO: check ZF referenceMap and dependantTables functional
     * 
     * 
     */
    
    /**
     * Database table's column for node id parameter.
     *
     * @var string
     */
    protected $_nestedIdColumn = null;
    
    /**
     * Column for external joining
     *
     * @var string
     */
    protected $_relativeKeyColumn = null;
    
    /**
     * Nested sets object
     *
     * @var Zend_Db_Tree_Nested
     */
    protected $_nested = null;
    
    /**
     * Nested tree id for the table
     *
     * @var integer
     */
    protected $_nestedTreeId = null;
    
    /**
     * Classname for row
     *
     * @var string
     */
    protected $_rowClass = 'Zend_Db_Tree_Nested_Table_Row';
    
    /**
     * Constructor
     * Config array may also contain 'nested' param with custom nested object
     *
     * @param array $config config for parent class
     */
    public function __construct($config = array())
    {
        // first, call parent construct (it may contains table name and we need it for nested configuration)
        parent::__construct($config);
        // then configure nested sets part
        if (isset($config[self::NESTED]) && ($config[self::NESTED] instanceof Zend_Db_Tree_Nested)) {
            $nested = $config[self::NESTED];
        } else {
            // create new instance of Zend_Db_Tree_Nested
            if ($this->_nestedTreeId === null) {
                throw new Zend_Db_Tree_Nested_Exception('Tree id for table is not specified.');
            }
            if (!$this->_name) {
                throw new Zend_Db_Tree_Nested_Exception('Table name is not specified');
            }
            $nestedConfig = array(
                'relativeTableName' => $this->_name,
            	'relativeKey'		=> $this->_relativeKeyColumn,
                'treeId'            => $this->_nestedTreeId,
            );
            $nested = new Zend_Db_Tree_Nested($nestedConfig);
        }
        $this->_nested = $nested;
        $this->_nestedIdColumn = $nested->getIdColumn();
        
        // We can work only with Zend_Db_Select return type
        $nested->setReturnType(Zend_Db_Tree_Nested::RETURN_OBJECT);
    }
    
    /**
     * Returns nested sets object
     *
     * @return Zend_Db_Tree_Nested
     */
    public function getNested()
    {
        return $this->_nested;
    }
    
    /**
     * Inserts a new row.
     * If relative key value is not set, create new node.
     *
     * @param  array  $data  Column-value pairs.
     * @return mixed         The primary key of the row inserted.
     */
    public function insert(array $data)
    {
        // Check relative key value
        if (!isset($data[$this->_relativeKeyColumn]) 
            || $data[$this->_relativeKeyColumn] === null) {
            // if it is not set, create new node in the root
            $nodeId = $this->_nested->createChildNodeLast();
            $data[$this->_relativeKeyColumn] = $nodeId;
        }
        $pkData = parent::insert($data);

        return $pkData;
    }
    
    /**
     * Override method for support Zend_Db_Select objects.
     * Support method for fetching rows.
     *
     * @param  Zend_Db_Table_Select|Zend_Db_Select $select  query options.
     * @return array An array containing the row results in FETCH_ASSOC mode.
     */
    protected function _fetch($select)
    {
        $stmt = $this->_db->query($select);
        $data = $stmt->fetchAll(Zend_Db::FETCH_ASSOC);
        return $data;
    }
    
    /**
     * Support method for fetching rowset
     *
     * @param Zend_Db_Select $select
     * @return mixed Rowset
     */
    protected function _fetchNestedRowset(Zend_Db_Select $select)
    {

        // Join data table
        $select->join($this->_name, $this->getAdapter()->quoteIdentifier($this->_name)
            . '.' . $this->getAdapter()->quoteIdentifier($this->_relativeKeyColumn)
            . '=' . $this->getAdapter()->quoteIdentifier(Zend_Db_Tree_Nested::TABLE_ALIAS 
            . '.' . $this->_nestedIdColumn));
        $rows = $this->_fetch($select);
        
        $data  = array(
            'table'    => $this,
            'data'     => $rows,
            'readOnly' => true,
            'rowClass' => $this->_rowClass,
            'stored'   => true
        );
        
        @Zend_Loader::loadClass($this->_rowsetClass);
        return new $this->_rowsetClass($data);
    }
    
    /**
     * Support method for fetching rows
     *
     * @param Zend_Db_Select $select
     * @return mixed Row
     */
    protected function _fetchNestedRow(Zend_Db_Select $select)
    {
        // Join data table
        $select->join($this->_name, // TODO: check quoteIdentifiers everywhere!
            $this->getAdapter()->quoteIdentifier($this->_name)
            . '.' . $this->getAdapter()->quoteIdentifier($this->_relativeKeyColumn)
            . '=' . $this->getAdapter()->quoteIdentifier(Zend_Db_Tree_Nested::TABLE_ALIAS 
            . '.' .$this->_nestedIdColumn));

        $rows = $this->_fetch($select);
        $row = $rows[0];
        // delete data from tree table  // TODO: check this and add if neccessary
        
        $data = array(
            'table'   => $this,
            'data'     => $row,
            'readOnly' => false,
            'stored'  => true
        );

        @Zend_Loader::loadClass($this->_rowClass);
        return new $this->_rowClass($data);
    }

    /**
     * Get node defined by $where
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     * @return Zend_Db_Table_Row_Abstract
     */
    protected function _getNode($where)
    {
        if (!($where instanceof Zend_Db_Table_Row_Abstract)) {
            if (is_numeric($where)) {
                $where = $this->_relativeKeyColumn.' = '.(int)$where;
            }
            
            $node = $this->fetchRow($where);

            if ($node === null) {
                return null;
            }
        } else {
            $node = $where;
        }

        return $node;
    }
    
    /**
     * Fetch whole tree
     *
     */
    public function fetchTree()
    {
        $select = $this->_nested->getTree();
        return $this->_fetchNestedRowset($select);
    }

    public function getBranch($where, $depth = null)
    {
        if (null === ($node = $this->_getNode($where))) {
             return false;
        }

        $select = $this->_nested->getBranch($node->{$this->_relativeKeyColumn});

        if ($depth) {
            $row = $this->_nested->getNode($node->{$this->_relativeKeyColumn},'array');
            $select->where('level <= ?', $row['level']+$depth);
        }

        return $this->_fetchNestedRowset($select);
    }
    
    /**
     * Fetch rowset with open tree for row defined by $where
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
	 * TODO: add @return to all methods
     */
    public function fetchOpenTree($where = null)
    {
        if ($where === null) {
            $select = $this->_nested->getRootNodes();
        } elseif (null === ($node = $this->_getNode($where))) {
            return null;
        } else {
            $select = $this->_nested->getOpenTree($node->{$this->_relativeKeyColumn});
        }
        return $this->_fetchNestedRowset($select);
    }
    
    /**
     * Fetch rowset with root nodes collection
     *
     * @return Zend_Db_Table_Rowset
     */
    public function fetchRootNodes()
    {
        $select = $this->_nested->getRootNodes();
        
        return $this->_fetchNestedRowset($select);
    }
    
    
	public function isRoot($where)
    {
        if (null === ($node = $this->_getNode($where))) {
            return false;
        }
                
        $node = $this->_nested->getNode($node->{$this->_relativeKeyColumn},'array');
        return $node['level'] == 1;
    }
    
    /**
     * Fetch rowset with path for row defined by $where
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     */
    public function fetchPath($where)
    {
        if (null === ($node = $this->_getNode($where))) {
            return null;
        }
        
        $select = $this->_nested->getPath($node->{$this->_relativeKeyColumn});
        return $this->_fetchNestedRowset($select);
    }
    
    /**
     * Fetch rowset with children for row defined by $where
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     */
    public function fetchChildren($where)
    {
        if (null === ($node = $this->_getNode($where))) {
            return null;
        }
        $select = $this->_nested->getChildren($node->{$this->_relativeKeyColumn});
        return $this->_fetchNestedRowset($select);
    }
    
    /**
     * Fetch rowset with descendats for row defined by $where
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     */
    public function fetchDescendants($where)
    {
        if (null === ($node = $this->_getNode($where))) {
            return null;
        }
        $select = $this->_nested->getDescendants($node->{$this->_relativeKeyColumn});
        return $this->_fetchNestedRowset($select);
    }
    
    /**
     * Fetch parent row for row defined by $where
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     */
    public function fetchParentRow($where)
    {
        if (null === ($node = $this->_getNode($where))) {
            return null;
        }
        $select = $this->_nested->getParent($node->{$this->_relativeKeyColumn});
        return $this->_fetchNestedRow($select);
    }
    
    /**
     * Delete the row
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $where OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     */
    public function delete($where, $startTransaction = true)
    {
        if (null === ($node = $this->_getNode($where))) {
            return null;
        }
        
        // Deletion block is put into db transaction
        // cause node from the tree has to be deleted too.
        if ($startTransaction) {
            $this->getAdapter()->beginTransaction();
        }

        $result = $this->_nested->delete($node->{$this->_relativeKeyColumn});
 
        //TODO костыль
        if ($startTransaction) {
            $this->getAdapter()->commit();
        }

        return $result;
    }
    
    /**
     * Inserts row as first child of another row.
     * Parent row should be define by where clause or by Row object.
     *
     * @param array $data Data for inserting
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $parentWhere OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.
     */
    public function insertChildFirst($data, $parentWhere = null)
    {
      	if (null === ($node = $this->_getNode($parentWhere))) {
            $parentNodeId = 0;
        } else {
            $parentNodeId = $node->{$this->_relativeKeyColumn};
        }

        // obtain new node id
        $nodeId = $this->_nested->createChildNodeFirst($parentNodeId);
        // set it to data
        $data[$this->_relativeKeyColumn] = $nodeId;
        return $this->insert($data);
    }
    
    /**
     * Inserts row as last child of another row.
     * Parent row should be define by where clause or by Row object.
     *
     * @param array $data Data for inserting
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $parentWhere OPTIONAL 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object. Or Row object.  
	 */
    public function insertChildLast($data, $parentWhere = null)
    {
        if (null === ($node = $this->_getNode($parentWhere))) {
            $parentNodeId = 0;
        } else {
            $parentNodeId = $node->{$this->_relativeKeyColumn};
        }

        // obtain new node id
        $nodeId = $this->_nested->createChildNodeLast($parentNodeId);
        // set it to data
        $data[$this->_relativeKeyColumn] = $nodeId;
        return $this->insert($data);
    }
    
    /**
     * Inserts row as next adjacent of another row.
     * Adjacent row should be define by where clause or by Row object.
     *
     * @param array $data Data for inserting
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $adjacent 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object. 
     */
    public function insertAdjacentNext($data, $adjacentWhere)
    {
     	if (null === ($node = $this->_getNode($adjacentWhere))) {
        	return null;
        }
        
        $adjacentNodeId = $node->{$this->_relativeKeyColumn};
        // obtain new node id
        $nodeId = $this->_nested->createAdjacentNodeNext($adjacentNodeId);
        // set it to data
        $data[$this->_relativeKeyColumn] = $nodeId;
        return $this->insert($data);
    }
    
    /**
     * Inserts row as previous adjacent of another row.
     * Adjacent row should be define by where clause or by Row object.
     *
     * @param array $data Data for inserting
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $adjacent 
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.  
     */
    public function insertAdjacentPrevious($data, $adjacentWhere)
    {
        if (null === ($node = $this->_getNode($adjacentWhere))) {
        	return null;
        }
        
        $adjacentNodeId = $node->{$this->_relativeKeyColumn};
        // obtain new node id
        $nodeId = $this->_nested->createAdjacentNodePrevious($adjacentNodeId);
        // set it to data
        $data[$this->_relativeKeyColumn] = $nodeId;
        return $this->insert($data);
    }
    
    /**
     * Move row to previous place in the tree on the level.
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $rowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.  
     */
    public function moveToPrevious($rowWhere)
    {
      	if (null === ($node = $this->_getNode($rowWhere))) {
            return null;
        }
        
        return $this->_nested->moveToPrevious($node->{$this->_relativeKeyColumn});
    }
    
    /**
     * Move row to next place in the tree on the level.
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $rowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.  
     */
    public function moveToNext($rowWhere)
    {
    	if (null === ($node = $this->_getNode($rowWhere))) {
            return null;
        }
        
        return $this->_nested->moveToNext($node->{$this->_relativeKeyColumn});
    }
    
    /**
     * Move one row to other and put it before it
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $rowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $moveToRowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.   
     */
    public function moveToRowPrevious($rowWhere, $moveToRowWhere)
    {
        if (null === ($row = $this->_getNode($rowWhere))) {
            return null;
        }

        if (null === ($moveToRow = $this->_getNode($moveToRowWhere))) {
            return null;
        }

        return $this->_nested->moveToNodePrevious(
            $row->{$this->_relativeKeyColumn}, 
            $moveToRow->{$this->_relativeKeyColumn}
        );
    }
    
    /**
     * Move one row to other and put it after it
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $rowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $moveToRowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.   
     */
    public function moveToRowNext($rowWhere, $moveToRowWhere)
    {
        if (null === ($row = $this->_getNode($rowWhere))) {
            return null;
        }

        if (null === ($moveToRow = $this->_getNode($moveToRowWhere))) {
            return null;
        }

        return $this->_nested->moveToNodeNext(
            $row->{$this->_relativeKeyColumn}, 
            $moveToRow->{$this->_relativeKeyColumn}
        );
    }
    
    /**
     * Move one row to other's children and put it first in the list.
     *
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $rowWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.
	 * @param string|array|Zend_Db_Table_Select|Zend_Db_Table_Row $parentWhere
	 * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.   
     */
    public function moveToChildrenFirst($rowWhere, $parentWhere)
    {
        if (null === ($node = $this->_getNode($rowWhere))) {
            return null;
        }

        if (null === ($parent = $this->_getNode($parentWhere))) {
            $parentId = 0;
        } else {
            $parentId = $parent->{$this->_relativeKeyColumn};
        }

        return $this->_nested->moveToChildrenFirst(
            $node->{$this->_relativeKeyColumn},
            $parentId
        );
    }
    
    /**
     * Move one row to other's children and put it last in the list.
     *
     * @param string|int|array|Zend_Db_Table_Select|Zend_Db_Table_Row $rowWhere
     * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.
     * @param string|int|array|Zend_Db_Table_Select|Zend_Db_Table_Row $parentWhere
     * 	  An SQL WHERE clause or Zend_Db_Table_Select object.  Or Row object.
     * @return boolean
     */
    public function moveToChildrenLast($rowWhere, $parentWhere)
    {
        if (null === ($node = $this->_getNode($rowWhere))) {
            return null;
        }

        if (null === ($parent = $this->_getNode($parentWhere))) {
            $parentId = 0;
        } else {
            $parentId = $parent->{$this->_relativeKeyColumn};
        }

        return $this->_nested->moveToChildrenLast(
            $node->{$this->_relativeKeyColumn},
            $parentId
        );
    }
}